﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace Mahanet.Abyss.PathCalculation
{
	public class Coordinate2DDataSourceMocker : IClock
	{
		#region Consts

		private const int APPROXIMATOR_DRAW_DELTA = 3;

		#endregion

		#region Fields

		private IDataDisplayer m_DataDisplayer;
		private Coordinate2DDataManager m_DataManager;
		private Coordinate2DDataGenerator m_DataGenerator;
		private Coordinate2DAimer m_Aimer;
		private DisplayerGunFacade m_GunFacade;

		private Random m_Random = new Random();
		private TickTimer m_TimelineTimer = null;
		private int m_NumberOfDataPoints;
		private int m_TargetInterceptionPoint;
		private int m_CurrentTick;
		private int m_MaxTickCount;
		private int m_AnimationTickInMs;
		private double m_SampleError;

		#endregion

		#region C'tor

		public Coordinate2DDataSourceMocker(IDataDisplayer dataDisplayer, Coordinate2DOracleFactory approximatorFactory)
		{
			this.m_DataDisplayer = dataDisplayer;
			this.m_DataGenerator = new Coordinate2DDataGenerator();

			m_GunFacade = new DisplayerGunFacade(dataDisplayer, this);
			m_Aimer = new Coordinate2DAimer(m_GunFacade, this);
			m_Aimer.DistanceToTravelTime = m_GunFacade.TravelTimeByDistance;
			m_Aimer.DistanceToAngularSkew = m_GunFacade.AngularSkewByDistance;
			m_Aimer.GunZeroAngleDirection = m_GunFacade.GunZeroAngleDirection;
			
			this.m_DataManager = 
				new Coordinate2DDataManager(m_DataGenerator, 
											approximatorFactory, 
											m_Aimer);
		}

		#endregion

		#region Public Methods

		public void GenerateAndDisplayPoints(double maxX, double maxY, int numberOfPoints,
											 int animationLengthInMs, int animationTickInMs, double sampleError)
		{
			if (m_TimelineTimer != null)
			{
				m_TimelineTimer.Stop();
			}

			m_DataDisplayer.ClearPoints();

			if (m_Aimer.DangerZone == null)
			{
				GenerateDangerZone(maxX, maxY);
			}

			m_DataDisplayer.MarkDangerZone(m_Aimer.DangerZone);

			m_GunFacade.RotationCenter = SetGunLocation(maxX, maxY);

			m_Aimer.GunRotationCenter = m_GunFacade.RotationCenter;

			m_DataDisplayer.AddDataPoint("Gun", m_GunFacade.RotationCenter.X, m_GunFacade.RotationCenter.Y, Colors.SkyBlue, 10);

			m_SampleError = sampleError;

			m_AnimationTickInMs = animationTickInMs;

			m_MaxTickCount = animationLengthInMs / animationTickInMs;

			m_NumberOfDataPoints = numberOfPoints;

			m_Aimer.MinimalDataSeriesIntervalInTicks = m_MaxTickCount / m_NumberOfDataPoints;

			m_TargetInterceptionPoint = (m_MaxTickCount / m_NumberOfDataPoints) * (m_NumberOfDataPoints - 1);

			m_DataGenerator.GenerateFunction(maxX, maxY, m_MaxTickCount);

			Coordinate2D targetInterceptionLocation = m_DataGenerator.GeneratedFunction(m_TargetInterceptionPoint);

			m_DataDisplayer.AddDataPoint("InterceptionSheker", targetInterceptionLocation.X, targetInterceptionLocation.Y, Colors.Purple, 6);

			BeginTimeline(m_MaxTickCount, (double)animationTickInMs);
		}

		public void GenerateStatisticData(double maxX, double maxY, int numberOfPoints,
											 int animationLengthInMs, int animationTickInMs, double sampleError)
		{
			if (m_TimelineTimer != null)
			{
				m_TimelineTimer.Stop();
			}

			m_SampleError = sampleError;

			m_MaxTickCount = animationLengthInMs / animationTickInMs;

			m_NumberOfDataPoints = numberOfPoints;

			m_TargetInterceptionPoint = (m_MaxTickCount / m_NumberOfDataPoints) * (m_NumberOfDataPoints - 1);

			CreateStatisticData(maxX, maxY);
		}

		public void GenerateDangerZone(double maxX, double maxY)
		{
		//	double dangerZoneX = maxX * (0.75 + m_Random.NextDouble() * 0.1);
		//	double dangerZoneMinY = maxY * (m_Random.NextDouble() * 0.5);
		//	double dangerZoneMaxY = maxY * (0.3 + m_Random.NextDouble() * 0.2);
			double dangerZoneX = maxX * (0.75 + m_Random.NextDouble() * 0.1);
			double dangerZoneMinY = maxY * (m_Random.NextDouble() * 0.4);
			double dangerZoneMaxY = maxY * (0.6 + m_Random.NextDouble() * 0.4);

			Segment2D dangerZone = 
				new Segment2D(new Coordinate2D(dangerZoneX, dangerZoneMinY), 
							  new Coordinate2D(dangerZoneX, dangerZoneMaxY));

			m_DataDisplayer.MarkDangerZone(dangerZone);

			m_Aimer.DangerZone = dangerZone;
		}

		public double GetCurrentTime()
		{
			return m_CurrentTick;
		}

		#endregion

		#region Private Methods

		private Coordinate2D SetGunLocation(double maxX, double maxY)
		{
			double locationX = maxX * (0.95 + 0.05 * m_Random.NextDouble());
			double locationY = maxY * (0.05 + 0.90 * m_Random.NextDouble());

			return new Coordinate2D(locationX, locationY);
		}

		private void CreateStatisticData(double maxX, double maxY)
		{
			const int NUMBER_OF_RUNS = 1000;
			int dataCreationInterval = m_MaxTickCount / m_NumberOfDataPoints;

			int interceptionCalculationTime = (m_MaxTickCount / m_NumberOfDataPoints) * (m_NumberOfDataPoints - 2);
			double totalDistance = 0.0;
			double totalDistancePercentage = 0.0;
			TimeSpan totalCalculationTime = new TimeSpan(0);

			for (int i = 0; i < NUMBER_OF_RUNS; i++)
			{
				m_DataGenerator.GenerateFunction(maxX, maxY, m_MaxTickCount);

				for (int t = 0; t <= interceptionCalculationTime; t += dataCreationInterval)
				{
					m_DataGenerator.GenerateDataPoint(t, m_SampleError);
				}

				DateTime calculationStart = DateTime.Now;
				var approximator = m_DataManager.GetApproximator(m_DataGenerator.LastDataSeriesId);
				approximator.GenerateApproximation();
				Coordinate2D calculatedPoint = approximator.Approximation(m_TargetInterceptionPoint);
				TimeSpan calculationTime = DateTime.Now - calculationStart;

				Coordinate2D actualPoint = m_DataGenerator.GeneratedFunction(m_TargetInterceptionPoint);

				double distance = calculatedPoint.DistanceTo(actualPoint);
				double distancePercentage = distance / actualPoint.DistanceTo(new Coordinate2D(0, 0));

				totalDistance += distance;
				totalDistancePercentage += distancePercentage;
				totalCalculationTime += calculationTime;
			}


			m_DataDisplayer.DisplayStatus(string.Format("After {0} runs: average distance [{1}], which is {2}% error. Avergae calculation time: {3}ms", 
														NUMBER_OF_RUNS,
														totalDistance / NUMBER_OF_RUNS,
														100 * totalDistancePercentage / NUMBER_OF_RUNS,
														((double)totalCalculationTime.TotalMilliseconds) / (double)NUMBER_OF_RUNS));
		}

		private void BeginTimeline(int maxT, double tInMs)
		{
			if (m_TimelineTimer != null)
			{
				m_TimelineTimer.Stop();
				m_TimelineTimer.Elapsed -= m_TimelineTimer_Elapsed;
			}

			m_TimelineTimer = new TickTimer(maxT, tInMs);
			m_TimelineTimer.Elapsed += m_TimelineTimer_Elapsed;

			m_TimelineTimer.Start();
		}

		private void m_TimelineTimer_Elapsed(int tick, int maxTick)
		{
			m_CurrentTick = tick;

			Coordinate2D point = m_DataGenerator.GeneratedFunction(tick);
			m_DataDisplayer.AddDataPoint("DataPoints", point.X, point.Y, Colors.Blue, 2);

			int dataCreationInterval = maxTick / m_NumberOfDataPoints;

			m_DataDisplayer.ClearPoints("Approximations" + (m_CurrentTick - 25));
			if (tick % dataCreationInterval == 0 && tick != maxTick)
			{
				m_DataGenerator.GenerateDataPoint(tick, m_SampleError);

				int numberOfPointsSoFar = 1 + (tick / dataCreationInterval);

				DisplayApproximation(numberOfPointsSoFar);

				DisplayErrorInInterceptionPoint(numberOfPointsSoFar);
			}

			if (Tick != null)
			{
				Tick();
			}

			m_DataDisplayer.DisplayStatus(string.Format("Tick {0} out of {1}", tick, maxTick));
		}

		private void DisplayErrorInInterceptionPoint(int numberOfPointsSoFar)
		{
			IPathOracle<double, Coordinate2D> approximator = m_DataManager.GetApproximator(m_DataGenerator.LastDataSeriesId);
			approximator.GenerateApproximation();

			Coordinate2D approximation = approximator.Approximation(m_TargetInterceptionPoint);
			Coordinate2D realValue = m_DataGenerator.GeneratedFunction(m_TargetInterceptionPoint);

			double difference = approximation.DistanceTo(realValue);

			m_DataDisplayer.AddDataPoint("Approximations" + m_CurrentTick, approximation.X, approximation.Y, Colors.DarkGreen, 4);
			m_DataDisplayer.MarkErrorInPoint(realValue, approximation, numberOfPointsSoFar, m_TargetInterceptionPoint);
		}

		private void DisplayApproximation(int numberOfPointsSoFar)
		{
			DataPoint<double, Coordinate2D> dataPoint = m_DataManager.LatestDataPoint;
			if (dataPoint == null)
			{
				return;
			}

			IPathOracle<double, Coordinate2D> approximator = m_DataManager.GetApproximator(m_DataGenerator.LastDataSeriesId);
			approximator.GenerateApproximation();

			m_DataDisplayer.AddDataPoint("DataPoints", dataPoint.Value.X, dataPoint.Value.Y);
			if (approximator is GraviticLeastSquaresCoordinate2DPathOracle)
			{
				var approx = approximator as GraviticLeastSquaresCoordinate2DPathOracle;
				if (approx.GeneratedXPoint != null && approx.GeneratedYPoint != null)
				{
					m_DataDisplayer.AddDataPoint("Approximations" + m_CurrentTick, approx.GeneratedXPoint.Y, approx.GeneratedYPoint.Y, Colors.Orange, 3);
				}
			}

			Color color = GetColorByDataPointCount(numberOfPointsSoFar);
			for (int t = 0; t <= m_MaxTickCount; t += APPROXIMATOR_DRAW_DELTA)
			{
				Coordinate2D point = approximator.Approximation(t);

				m_DataDisplayer.AddDataPoint("Approximations" + m_CurrentTick, point.X, point.Y, color, 2);
			}
		}

		private Color GetColorByDataPointCount(int dataPoints)
		{
			double p = (double)dataPoints / m_NumberOfDataPoints;
			Color certainColor = Colors.DarkGreen;
			Color uncertainColor = Colors.Pink;

			var red = uncertainColor.R + (int)((certainColor.R - uncertainColor.R) * p);
			var green = uncertainColor.G + (int)((certainColor.G - uncertainColor.G) * p);
			var blue = uncertainColor.B + (int)((certainColor.B - uncertainColor.B) * p);

			return Color.FromRgb((byte)red, (byte)green, (byte)blue);
		}

		#endregion

		public event Action Tick;

		public double ConvertTicksToMs(double ticks)
		{
			return ticks * m_AnimationTickInMs;
		}
	}
}
